/*
 * Copyright (c) 2006-2021, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2021-01-07     ChenYong     first version
 * 2021-12-20     armink       add multi-instance version
 */

#include <rtthread.h>
#include <dfs_file.h>
#include <unistd.h>
#include <dirent.h>

#include <ulog.h>
#include <ulog_be.h>
#include "config_api.h"
#include "ulog_app.h"

#ifdef ULOG_BACKEND_USING_FILE

#if defined(ULOG_ASYNC_OUTPUT_THREAD_STACK) && (ULOG_ASYNC_OUTPUT_THREAD_STACK < 2048)
#error "The value of ULOG_ASYNC_OUTPUT_THREAD_STACK must be greater than 2048."
#endif

typedef struct _file_info {
    char name[ULOG_FILE_PATH_LEN];
    // struct stat info;
} file_info_t, *file_info_p;

/**
* @brief    判断文件是否被打开
* @attention 不同于rt-thread/components/dfs/src/dfs.c的fd_is_open函数
* @param[IN]    pathname 
* @return int 已打开则返回文件描述符，否则返回-1
* @retval > 0 成功
* @retval <= 0 出错
*/
static int file_opened(const char *pathname)
{
    int ret = 0;
    char *fullpath;
    unsigned int index;
    struct dfs_filesystem *fs;
    struct dfs_fd *fd;
    struct dfs_fdtable *fdt;

    fdt = dfs_fdtable_get();
    fullpath = dfs_normalize_path(NULL, pathname);
    if (NULL == fullpath)
    {
        return -1;
    }
    char *mountpath;
    fs = dfs_filesystem_lookup(fullpath);
    if (fs == NULL)
    {
        /* can't find mounted file system */
        rt_free(fullpath);
        return -1;
    }

    /* get file path name under mounted file system */
    if (fs->path[0] == '/' && fs->path[1] == '\0')
        mountpath = fullpath;
    else
        mountpath = fullpath + strlen(fs->path);

    dfs_lock();

    for (index = 0; index < fdt->maxfd; index++)
    {
        fd = fdt->fds[index];
        if (fd == NULL || fd->fops == NULL || fd->path == NULL) continue;

        if ((fd->fs == fs) && (strcmp(fd->path, mountpath) == 0))
        {
            ret = index + DFS_FD_OFFSET;
            break;
        }
    }
    dfs_unlock();
    rt_free(fullpath);
    return ret;
}

/**
* @brief    文件重命名
* @param[IN]    old_name 
* @param[IN]    new_name 
* @return int 0: 成功，其他：失败
*
*/
static int rename_file(const char *old_name, const char *new_name)
{
    int ret = 0;
    if (access(new_name, 0) == 0)
    {
        // 文件存在则删除
        int fd = file_opened(new_name);
        if (fd > 0)
        {
            close(fd);
        }
        ret = unlink(new_name);
        if (ret)
        {
            LOG_E("rename_file of unlink %s", new_name);
            return ret;
        }
    }
    ret = rename(old_name, new_name);
    return ret;
}

/**
* @brief    查找特定字串文件
* @param[IN]    dir         目录绝对路径
* @param[IN]    sub_name    子目录名
* @param[OUT]   full_name   完整文件名
* @return int 0: 成功，其他：失败
*
*/
static int find_file(const char *dir, const char *sub_name, char *full_name)
{
    int ret = -1;
    DIR *dirp;
    struct dirent *direntp;

    if ((NULL == dir) || (NULL == sub_name) || (NULL == full_name))
    {
        return -1;
    }

    dirp = opendir(dir);
    if (NULL == dirp)
    {
        return -1;
    }

    while ((direntp = readdir(dirp)) != NULL)
    {
        if (rt_strstr(direntp->d_name, sub_name) != NULL)
        {
            rt_snprintf(full_name, ULOG_FILE_PATH_LEN, "%s/%s", dir, direntp->d_name);
            ret = 0;
            break;
        }
    }

    closedir(dirp);
    return ret;
}

int ulog_find_file(ulog_file_be_t *be, const char *dir, const char *sub_name, char opened)
{
    int ret = -1;
    if ((NULL == be) || (NULL == dir) || (NULL == sub_name))
    {
        return -1;
    }
    ret = find_file(dir, sub_name, be->cur_log_file_path);
    if (!ret && opened)
    {
        be->cur_log_file_fd = open(be->cur_log_file_path, O_RDWR);
    }
    return ret;
}

static int compare_files(const void *a, const void *b) 
{
    int ret = 0;
    file_info_t *file_a = (file_info_t *)a;
    file_info_t *file_b = (file_info_t *)b;
    ret = rt_strncmp(file_a->name, file_b->name, ULOG_FILE_PATH_LEN);
    return ret;
}

int ulog_trim_file(ulog_file_be_t *be)
{
    DIR *dir;
    struct dirent *entry;
    char *file_name = rt_calloc(1, ULOG_FILE_PATH_LEN);
    file_info_t *file_list = rt_calloc(be->file_max_num * 2, sizeof(file_info_t));
    
    dir = opendir(be->cur_log_dir_path);
    if (NULL == dir)
    {
        rt_kprintf("%s opendir failed\r\n", be->cur_log_dir_path);
        return -1;
    }
    be->cur_log_file_cnt = 0;
    // 遍历文件夹
    while ((entry = readdir(dir)) != NULL)
    {
        if ((rt_strcmp(entry->d_name, ".") == 0) || (rt_strcmp(entry->d_name, "..") == 0))
        {
            continue; // 跳过当前目录和父目录
        }
        if ((rt_strlen(entry->d_name) <= 8) || 
            (rt_strncmp(entry->d_name, be->parent.name, rt_strlen(be->parent.name))))
        {
            rt_snprintf(file_name, ULOG_FILE_PATH_LEN, "%s/%s", be->cur_log_dir_path, entry->d_name);
            unlink(file_name);
            continue;
        }
        if (rt_strstr(entry->d_name, ULOG_TMP_NAME) != NULL)
        {
            continue;
        }
        rt_strncpy(file_list[be->cur_log_file_cnt].name, entry->d_name, ULOG_FILE_PATH_LEN);
        be->cur_log_file_cnt++;
        if (be->cur_log_file_cnt >= be->file_max_num * 2)
        {
            break;
        }
    }
    if (be->cur_log_file_cnt > 0)
    {
        // rt_kprintf("ulog_trim_file sort (%d) files date\r\n", be->cur_log_file_cnt);
        qsort(file_list, be->cur_log_file_cnt, sizeof(file_info_t), compare_files);
        // for (int i = 0; i < be->cur_log_file_cnt; i++)
        // {
        //     rt_kprintf("ulog_trim_file (%d) %s\r\n", i, file_list[i].name);
        // }
        if (be->cur_log_file_cnt >= be->file_max_num)
        {
            // rt_kprintf("ulog_trim_file delete (%d) files ", be->cur_log_file_cnt - be->file_max_num + 1);
            for (int i = 0; i < (be->cur_log_file_cnt - be->file_max_num + 1); i++)
            {
                rt_snprintf(file_name, ULOG_FILE_PATH_LEN, "%s/%s", be->cur_log_dir_path, file_list[i].name);
                // rt_kprintf("ulog_trim_file rm %s\r\n", file_name);
                unlink(file_name);
            }
            be->cur_log_file_cnt = be->file_max_num - 1;
            // rt_kprintf("still have (%d) files\r\n", be->cur_log_file_cnt);
        }
    }
    if (NULL != dir)
    {
        closedir(dir);
    }
    rt_free(file_list);
    rt_free(file_name);
    return 0;
}

// 重命名tmp文件
static rt_bool_t ulog_tmpfile_rename(ulog_file_be_t *be)
{
    // mv xxx_n_tmp.log => xxx_n.log, example: xxx_0_tmp.log => xxx_0.log
    int new_name_len = 0;
    const int suffix_len = rt_strlen("_tmp.log");
    char old_path[ULOG_FILE_PATH_LEN] = {0};

    if (be->cur_log_file_fd >= 0)
    {
        close(be->cur_log_file_fd);
        be->cur_log_file_fd = -1;
    }

    rt_strncpy(old_path, be->cur_log_file_path, ULOG_FILE_PATH_LEN);
    new_name_len = rt_strnlen(old_path, ULOG_FILE_PATH_LEN) - suffix_len;
    rt_strncpy(be->cur_log_file_path, old_path, new_name_len);
    rt_memcpy(be->cur_log_file_path + new_name_len, ".log", sizeof(".log"));
    // rt_kprintf("rename file %s -> %s\r\n", old_path, be->cur_log_file_path);

    return rename_file(old_path, be->cur_log_file_path);
}

static void ulog_file_backend_flush_with_buf(struct ulog_backend *backend)
{
    ulog_file_be_t *be = (ulog_file_be_t *)backend;
    rt_size_t file_size = 0, write_size = 0;
    gw_base_cfg_t *cfg = uc_static_get_gateway_base_cfg();
    struct tm *tm, tm_tmp;
    struct timeval now;
    time_t t;

    if (be->enable == RT_FALSE || be->buf_ptr_now == be->file_buf)
    {
        return;
    }

    if (be->cur_log_file_fd < 0)
    {
        /* check log file directory  */
        if (access(be->cur_log_dir_path, 0) < 0)
        {
            mkdir(be->cur_log_dir_path, 0);
        }

        if (be->cur_log_file_cnt >= be->file_max_num)
        {
            // 删除旧日志文件
            if (ulog_trim_file(be))
            {
                return;
            }
        }
        be->cur_log_file_cnt++;
        be->cur_log_file_seq++;
        // 获取时间戳
        if (gettimeofday(&now, RT_NULL) >= 0)
        {
            t = now.tv_sec;
        }
        tm = localtime_r(&t, &tm_tmp);
        // 生成文件名
        // 格式: 日志目录/前缀_设备ID_年月日_时分秒_文件序号_tmp.log
        // 例如: /logs/uc_info_1_240513_123456_1_tmp.log
        rt_snprintf(be->cur_log_file_path, ULOG_FILE_PATH_LEN, 
                    "%s/%s_%d_%02d%02d%02d_%02d%02d%02d_%d_tmp.log", 
                    be->cur_log_dir_path, be->parent.name, cfg->dev_id, 
                    tm->tm_year - 100, tm->tm_mon + 1, tm->tm_mday, 
                    tm->tm_hour, tm->tm_min, tm->tm_sec, be->cur_log_file_seq); 

        be->cur_log_file_fd = open(be->cur_log_file_path, O_CREAT | O_RDWR | O_APPEND);
        if (be->cur_log_file_fd < 0)
        {
            return;
        }
    }

    file_size = lseek(be->cur_log_file_fd, 0, SEEK_END);
    if (file_size >= (be->file_max_size - be->buf_size * 2))
    {
        ulog_tmpfile_rename(be);
        return;
    }

    write_size = (rt_size_t)(be->buf_ptr_now - be->file_buf);
    /* write to the file */
    if (write(be->cur_log_file_fd, be->file_buf, write_size) != write_size)
    {
        return;
    }
    /* flush file cache */
    fsync(be->cur_log_file_fd);

    /* point be->buf_ptr_now at the head of be->file_buf[be->buf_size] */
    be->buf_ptr_now = be->file_buf;
}

static void ulog_file_backend_output_with_buf(struct ulog_backend *backend, rt_uint32_t level,
            const char *tag, rt_bool_t is_raw, const char *log, rt_size_t len)
{
    ulog_file_be_t *be = (ulog_file_be_t *)backend;
    rt_size_t copy_len = 0, free_len = 0;
    const unsigned char *buf_ptr_end = be->file_buf + be->buf_size;

    while (len)
    {
        /* free space length */
        free_len = buf_ptr_end - be->buf_ptr_now;
        /* copy the log to the mem buffer */
        if (len > free_len)
        {
            copy_len = free_len;
        }
        else
        {
            copy_len = len;
        }
        rt_memcpy(be->buf_ptr_now, log, copy_len);
        /* update data pos */
        be->buf_ptr_now += copy_len;
        len -= copy_len;
        log += copy_len;

        RT_ASSERT(be->buf_ptr_now <= buf_ptr_end);
        /* check the log buffer remain size */
        if (buf_ptr_end == be->buf_ptr_now)
        {
            ulog_file_backend_flush_with_buf(backend);
            if (buf_ptr_end == be->buf_ptr_now)
            {
                /* There is no space, indicating that the data cannot be refreshed
                   to the back end of the file Discard data and exit directly */
                break;
            }
        }
    }
}

/* initialize the ulog file backend */
int ulog_file_backend_init(ulog_file_be_t *be, const char *name, const char *dir_path, rt_size_t max_num,
        rt_size_t max_size, rt_size_t buf_size)
{
    be->file_buf            = rt_calloc(1, buf_size);
    be->cur_log_dir_path    = rt_calloc(1, ULOG_FILE_PATH_LEN);
    be->cur_log_file_path   = rt_calloc(1, ULOG_FILE_PATH_LEN);
    if ((!be->file_buf) || (!be->cur_log_dir_path) || (!be->cur_log_file_path))
    {
        LOG_W("Warning: NO MEMORY for %s file backend\n", name);
        return -RT_ENOMEM;
    }
    /* temporarily store the start address of the ulog file buffer */
    be->buf_ptr_now         = be->file_buf;
    be->cur_log_file_cnt    = 0;
    be->cur_log_file_seq    = 0;
    be->cur_log_file_fd     = -1;
    be->file_max_num        = max_num;
    be->file_max_size       = max_size;
    be->buf_size            = buf_size;
    be->enable              = RT_FALSE;
    rt_strncpy(be->cur_log_dir_path, dir_path, ULOG_FILE_PATH_LEN);
    /* the buffer length MUST less than file size */
    RT_ASSERT(be->buf_size < be->file_max_size);

    be->parent.output    = ulog_file_backend_output_with_buf;
    be->parent.flush     = ulog_file_backend_flush_with_buf;
    return ulog_backend_register((ulog_backend_t) be, name, RT_FALSE);
}

/* uninitialize the ulog file backend */
int ulog_file_backend_deinit(ulog_file_be_t *be)
{
    if (be->cur_log_file_fd >= 0)
    {
        /* flush log to file */
        ulog_file_backend_flush_with_buf((ulog_backend_t)be);
        /* close */
        close(be->cur_log_file_fd);
        be->cur_log_file_fd = -1;
    }

    if (be->file_buf)
    {
        rt_free(be->file_buf);
        be->file_buf = RT_NULL;
    }
    if (be->cur_log_file_path)
    {
        rt_free(be->cur_log_file_path);
        be->cur_log_file_path = RT_NULL;
    }
    if (be->cur_log_dir_path)
    {
        rt_free(be->cur_log_dir_path);
        be->cur_log_dir_path = RT_NULL;
    }

    ulog_backend_unregister((ulog_backend_t)be);
    return 0;
}

void ulog_file_backend_enable(ulog_file_be_t *be)
{
    be->enable = RT_TRUE;
}

void ulog_file_backend_disable(ulog_file_be_t *be)
{
    be->enable = RT_FALSE;
}

#endif /* ULOG_BACKEND_USING_FILE */
